在現實世界的開發環境中,我們不可能永遠碰到最簡單的業務狀況,一定會有不同的複雜溝通需要處理。而在複雜的微服務架構中,如何確保多個服務之間的協作不僅高效且容易維護則是一件值得考慮的事情。在 Anser 的協作器中除了順序執行的 Step 以外,也能夠透過在單個 Step 中加入一個以上的 Action 達成更複雜的微服務溝通。
在這裡,我們試想一個需求:
上述的需求雖然有一些詭異,但我們永遠無法在現實世界中阻止這些不合理的事情發生。而 Anser 的服務協作器在這個情況下就能很好地派上用場,我們如何在不修改微服務的情況下透過協作器指揮現有功能滿足新需求,便是這一章的要點。
進入你的  Anser-Tutorial-Service 資料夾中,若你的本地開發環境還未有這些內容,請參考第 12 章與第 4 章的內容建立起你的本地開發環境。
定位到 {project_root}/Services/ProductionService.php 你應該能看見這兩個公開方法:
/**
 * 更新商品資訊
 *
 * @param ModifyProduct $modifyProduct
 * @return ActionInterface
 */
public function updateProductAction(ModifyProduct $modifyProduct): ActionInterface {
    return $this->getAction(
        method: "PUT",
        path: "/api/v1/products/{$modifyProduct->p_key}"
    )->setOptions([
        "json" => $modifyProduct->toArray()
    ]);
}
/**
 * 取得商品資訊
 *
 * @param integer $productId
 * @return ActionInterface
 */
public function productInfoAction(int $productId): ActionInterface
{
    return $this->getAction(
        method: "GET",
        path: "/api/v1/products/{$productId}"
    );
}
這兩個方法分別代表了我們要在協作器中使用的兩個主要行動:「更新商品資訊」和「取得商品資訊」。 updateProductAction 方法接受一個 ModifyProduct 物件作為參數。這個物件包含了我們要修改的商品的關鍵資訊(如價格、名稱、描述等)。此方法會回傳一個行動(Action),該行動的功能是發送一個 PUT 請求到商品服務的 API,藉此更新特定的商品資訊。
productInfoAction 方法則是用來取得特定商品的資訊。它只需要一個商品 ID 作為參數,然後回傳一個行動(Action),該行動會發送一個 GET 請求到商品服務的 API,從而取得該商品的所有資訊。
接著我們將檔案定位至 {project_root}/Services/Models/ModifyProduct.php ,筆者非常推薦以這種方式實作你需要於多個類別傳遞的特定資料結構。
<?php
namespace Services\Models;
class ModifyProduct
{
    public int $p_key;
    public ?string $name = null;
    public ?string $description = null;
    public ?int $price = null;
    
    /**
     * OrderProductDetail constructor.
     *
     * @param array $data
     */
    public function __construct(array $data)
    {
        $this->p_key = (int)$data['p_key'];
        $this->name = $data['name'] ?? null;
        $this->description = $data['description'] ?? null;
        $this->price = $data['price'] ?? null;
    }
    
    public function toArray(): array
    {
        $returnArray = [
            "p_key" => $this->p_key,
        ];
        if(!is_null($this->name)){
            $returnArray['name'] = $this->name;
        }
        if(!is_null($this->description)){
            $returnArray['description'] = $this->description;
        }
        if(!is_null($this->price)){
            $returnArray['price'] = $this->price;
        }
        return $returnArray;
    }
}
透過這種設計方法可以確保資料傳遞時的完整性以及提供統一的存取介面,在這個 ModifyProduct 類別中,我們對商品的所有屬性進行了型別上的定義,而 name、description 和 price 則是可以為 null,這意味著它們是可選的。
這種資料模型類別在許多場景下都特別有用。當你需要將資料在多個類別、服務或不同程式層級之間傳遞時,使用這種資料模型可以使得資料的流動更加清晰和有組織。此外,當涉及到資料的修改或操作時,這樣的資料模型還可以確保所有操作都基於同一組資料規範,避免了潛在的不一致性。
之後來到我們的重頭戲,協作器的主要邏輯,你可以在你的 {project_root}/Orchestrators 中建立起 ProductsModifyOrchestrator 並鍵入以下內容:
<?php
namespace Orchestrators;
use SDPMlab\Anser\Orchestration\Orchestrator;
use Services\ProductionService;
use Services\Models\ModifyProduct;
class ProductsModifyOrchestrator extends Orchestrator
{
    protected $productionService;
    public function __construct()
    {
        $this->productionService = new ProductionService();
    }
    /**
     * definition of orchestrator
     *
     * @param ModifyProduct[] $products
     * @return void
     */
    protected function definition(array $modifyProducts=[])
    {
        //修改產品資訊
        $step0 = $this->setStep();
        foreach($modifyProducts as $index => $modifyProduct){
            $step0->addAction(
                alias: 'modify_' . ($index+1),
                action: $this->productionService->updateProductAction($modifyProduct)
            );
        }
        //取得產品資訊
        $step1 = $this->setStep();
        foreach($modifyProducts as $index => $modifyProduct){
            $step1->addAction(
                alias: 'product_' . ($index+1),
                action: $this->productionService->productInfoAction($modifyProduct->p_key)
            );
        }
    }
    protected function defineResult(): array
    {
        $actionList = $this->getStep(1)->getStepActionList();
        $data = [
            "success" => true,
            "message" => "協作器執行成功",
            "data" => []
        ];
        foreach($actionList as $action){
            $data['data'][] = $action->getMeaningData()['data'];
        }
        return $data;
    }
}
在這個範例中,我們定義了 $step0 與 $step1 (沒錯,Anser 協作器由 0 開始記數),它們都以 Foreach 遍歷商品的資料,並作了一些處理。
在 step0 中,我們將每個 ModifyProduct 都以 updateProductAction() 建立起 Actions,這意味著我們會在這個步驟中更新所有商品的資訊。
然後,在 step1 中,我們對每個已經更新的商品傳入 productInfoAction() 並建立起 Actions,這確保了在所有商品更新之後,我們可以取得它們的最新資訊。

整個生命週期下的請求順序
在這個範例下,雖然步驟與步驟之間維持著明顯的先後關係,但在同一個步驟下,所有的行動將以並行連線的方式向服務做出請求,這將會使整個編排器保持著最好的效能。接著關注 defineResult() :
$actionList = $this->getStep(1)->getStepActionList();
我們可以直接透過 getStep(int $stepNumber) 方法取得在 definition() 中建立的 Step 實體。當然,前提是你沒有忘記或者是輸入成錯的步驟號碼。
接著我們透過 Step::getStepActionList() 方法,一次取得在一個步驟下的所有 Actions 。當然,在 defineResult() 被執行的情況下,所有的 Action 一定是保持著 Success 的狀態。
最後,你可以建立起程式的進入點 {project_root/modify_products.php}:
<?php
require_once './init.php';
use Services\Models\ModifyProduct;
use Orchestrators\ProductsModifyOrchestrator;
$productList = array_map(function($product){
    return new ModifyProduct($product);
}, json_decode(file_get_contents('php://input'), true));
$productOrch = new ProductsModifyOrchestrator();
$result   = $productOrch->build($productList);
header("Content-Type: application/json");
echo json_encode($result);
在主要的執行檔案中,我們先將輸入的 JSON Body 轉換為以 ModifyProduct 組成的物件列表,然後使用 ProductsInfoOrchestrator 來進行微服務協作。
打開你的 Postman ,試試看這個輸入值:
{{tutorial_service}}/modify_products.php
POST
[
    {
        "p_key" : 23,
        "price" : 5322
    },
    {
        "p_key" : 88,
        "description" : "Modify Des"
    },
    {
        "p_key" : 77,
        "name" : "Modify Product Name",
        "description" : "Modify descriptionn",
        "price" : 45
    }
]

看起來一切都十分正確!
但有一個問題,在這個編排器中,若我們重複執行相同的請求,微服務將會回應錯誤,因為重複輸入的數值與目前的最新產品資訊相同。

在上述的協作器中我們並沒有處理錯誤時的回傳內容,因此只要錯誤一發生協作器將會直接拋出錯誤例外。在這個章節的最後我想將這個任務轉交給你,請你依照第 13 章與前面學習到的知識,完善這個協作器。
在這個章節中,我們學習到了如何透過在一個步驟中加入多個行動,在順序執行的大框架下也給予了相對的彈性處理並行處理的 Actions 。透過順序與並行交替的協作指揮,可以在不直接修改既有微服務的情況下,進行複雜的業務邏輯協作。